Golang 线程安全的 sync.Map 以及使用时遇到的问题

前言

最近在使用 Golang 实现 HNSW 算法的多属性支持,在其中用到的 map 结构。然后 Map 并不是线程安全的,使用多线程的话在新版本的 Golang 中会直接出现 panic。

sync.Map 这个数据结构是线程安全的,它填补了 Map 线程不安全的缺陷,不过最好只在需要的情况下使用。它一般用于并发模型中对同一类 map 结构体的读写,或其他适用于 sync.Map 的情况。

关于 sync.Map 的源码解析文章:Go 1.9 sync.Map揭秘

sync.Map使用方法

  1. Store 存 key,value
  2. LoadOrStore 取&存—具体看代码
  3. Load 取 key 对应的 value
  4. Range 遍历所有的 key,value
  5. Delete 删除 key,及其 value

Store(key, value interface{})

说明: 存储一个设置的键值。

LoadOrStore(key, value interface{}) (actual interface{}, loaded bool)

说明: 返回键的现有值(如果存在),否则存储并返回给定的值,如果是读取则返回true,如果是存储返回false。

Load(key interface{}) (value interface{}, ok bool)

说明: 读取存储在map中的值,如果没有值,则返回nil。OK的结果表示是否在map中找到值。

Delete(key interface{})

说明: 删除键对应的值。

Range(f func(key, value interface{}) bool)

说明: 循环读取map中的值。

代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
package main

import (
"fmt"
"sync"
)

func main() {
var m sync.Map

//Store
m.Store(1, "a")
m.Store(2, "b")

//LoadOrStore
//若key不存在,则存入key和value,返回false和输入的value
v, ok := m.LoadOrStore("1", "aaa")
fmt.Println(ok, v) //false aaa

//若key已存在,则返回true和key对应的value,不会修改原来的value
v, ok = m.LoadOrStore(1, "aaa")
fmt.Println(ok, v) //false aaa

//Load
v, ok = m.Load(1)
if ok {
fmt.Println("it's an existing key,value is ", v)
} else {
fmt.Println("it's an unknown key")
}

//Range
//遍历sync.Map, 要求输入一个func作为参数
f := func(k, v interface{}) bool {
//这个函数的入参、出参的类型都已经固定,不能修改
//可以在函数体内编写自己的代码,调用map中的k,v

fmt.Println(k, v)
return true
}
m.Range(f)

//Delete
m.Delete(1)
fmt.Println(m.Load(1))
}

// false aaa
// true a
// it's an existing key,value is a
// 1 a
// 2 b
// 1 aaa
// <nil> false

实际运用时遇到的问题

1
fatal error: sync: unlock of unlocked mutex

在多线程时会遇到这样的错误,十分的随机且不可定位,在 Go 的 github 上找到这样类似问题的 issue,Go 的开发人员提到 sync.Map 的使用规则:

A Map must not be copied after first use.

Package sync provides basic synchronization primitives such as mutual exclusion locks. Other than the Once and WaitGroup types, most are intended for use by low-level library routines. Higher-level synchronization is better done via channels and communication.

Values containing the types defined in this package should not be copied.

创建完成的 sync.Map 是线程安全的,但是经过拷贝之后,两个 sync.Map 里面存储的是同一个 map(就是那个原生的,线程不安全的 map), mutex 无法起到保护作用,就线程不安全了。

参考:

sync.RWMutex+Map

利用传统的 sync.RWMutex+Map 实现并发安全的 Map:

1
2
3
4
var rwmap = struct{
sync.RWMutex
m map[string]string
}{m: make(map[string]sring)}

读数据时候,读锁锁定:

1
2
3
4
rwmap.RLock()
value:= rwmap.m["key"]
rwmap.RUnlock()
fmt.Println("key:", value)

写数据的时候,写锁锁定:

1
2
3
rwmap.Lock()
rwmap.m["key"]="value"
rwmap.Unlock()

两种 Map 性能对比

image-20190522180238496

0%